<?php

namespace pmvc\remoting\jsonrpc;

use pmvc\util\ClassUtil;
use pmvc\log\Log;

use Exception;
use ReflectionClass;

/**
 * A client for accessing JSON-RPC over HTTP servers.
 *
 */
class JsonRpcClient {

	private $url  				= false;
	private $reuseConnections	= true;

	private static $curl = null;
	private $nextId = 99;
	

	/**
	 * Creates the client for the given URL.
	 * @param stirng $url
	 */
	public function __construct($url) {
		$this->url = $url;
	}

	/**
	 * Returns a curl resource.
	 * @return resource
	 */
	private function getCurl() {
		if (!isset(self::$curl)) {

			// initialize
			self::$curl = curl_init();

			// set options
			curl_setopt(self::$curl, CURLOPT_FAILONERROR, 		true);
			curl_setopt(self::$curl, CURLOPT_FOLLOWLOCATION, 	true);
			curl_setopt(self::$curl, CURLOPT_FORBID_REUSE, 		$this->reuseConnections===false);
			curl_setopt(self::$curl, CURLOPT_FRESH_CONNECT, 	$this->reuseConnections===false);
			curl_setopt(self::$curl, CURLOPT_CONNECTTIMEOUT, 	5);
			
		}
		return self::$curl;
	}

	/**
	 * Invokes the given method with the given arguments
	 * on the server and returns it's value.  If {@code $returnType}
	 * is specified than an instance of the class that it names
	 * will be created passing the json object (stdClass) to it's
	 * constructor.
	 * 
	 * @param string $method the method to invoke
	 * @param Array $params the parameters (if any) to the method
	 * @param string $id the request id
	 * @param Array $headers any additional headers to add to the request
	 */
	public function invoke($method, Array $params=Array(), $id=false, Array $headers=Array()) {

		// get curl
		$curl = $this->getCurl();

		// set url
		curl_setopt($curl, CURLOPT_URL, $this->url);

		// set post body
		$request = json_encode(Array(
			'jsonrpc'	=> '2.0',
			'method'	=> $method,
			'params'	=> $params,
			'id'		=> ($id===false) ? ++$this->nextId : $id
		));
		Log::trace(__CLASS__, "json-rpc request: {0}", $request);
		curl_setopt($curl, CURLOPT_POST, 			true);
		curl_setopt($curl, CURLOPT_POSTFIELDS, 		$request);
		curl_setopt($curl, CURLOPT_RETURNTRANSFER, 	true);

		// set headers
		$curlHeaders = Array();
		$curlHeaders []= "Content-type: application/json-rpc";
		for (reset($headers); list($key, $value)=each($headers); ) {
			$curlHeaders []= $key.": ".$value."\n";
		}
		curl_setopt($curl, CURLOPT_HTTPHEADER, $curlHeaders);

		// post the data
		$response = curl_exec($curl);
		if (!$response) {
			throw new Exception('cURL error '.curl_error($curl).' while making request to '.$this->url);
		}

		// decode json response
		$response = json_decode($response);
		if ($response==NULL || curl_error($curl)!=0) {
			throw new Exception("JSON parsing error occured: ".json_last_error());

		// throw errors
		} else if (isset($response->error)) {
			$msg = 'JSON-RPC error';
			if (isset($response->error->message) && !empty($response->error->message)) {
				$msg .= ': "' . $response->error->message . '"';
			}
			$msg .= "\n";
			$msg .= 'URL: ' . $this->url;
			$msg .= "\n";
			$msg .= 'Method: ' . $method;
			$msg .= "\n";
			$msg .= 'Arguments: ' . self::printArguments($params, 2);
			
			if (isset($response->error->code)) {
				throw new Exception($msg, intval($response->error->code));
			} else {
				throw new Exception($msg);
			}
	    }

	    // return the data
		return $response->result;
	}

	/**
	 * Printing arguments.
	 * @param $arg
	 * @param $depth
	 */
	private static function printArguments($args, $depth=1) {
		$argStrings = Array();
		foreach ($args as $arg) {
			$argStrings[] = self::printArgument($arg, $depth);
		}
		return implode($argStrings, ', ');
	}

	/**
	 * Print an argument.
	 * @param $arg
	 * @param $depth
	 */
	private static function printArgument($arg, $depth=1) {
		if ($arg === NULL) {
			return 'NULL';
		} else if (is_array($arg)) {
			if ($depth > 1) {
				return '[' . self::printArguments($arg, ($depth - 1)) . ']';
			} else {
				return 'Array';
			}
		} else if (is_object($arg)) {
			return 'Object';
		} else if (is_bool($arg)) {
			return ($arg === TRUE) ? 'true' : 'false';
		} else if (is_string($arg)) {
			return "'$arg'";
		}
		return strval($arg);
	}
}

?>